package in.co.codedoc.json;

import in.co.codedoc.cg.TypeUtil;
import in.co.codedoc.cg.annotations.IsAnIOCComponent;
import in.co.codedoc.ioc.Initializable;
import in.co.codedoc.util.PropertiesUtil;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

@IsAnIOCComponent
public class JSONUtil
	implements Initializable
{
	@SuppressWarnings("unchecked")
	@Override
	public void Initialize()
	{
		encoderMethodByType = new HashMap<Class, Method>();
		decoderMethodByType = new HashMap<Class, Method>();
		
		ScanClasspath();
	}	
	
	public static JSONBooleanValue Encode(boolean b,String typeName)
	{
		return new JSONBooleanValue(b);
	}
	
	public static JSONLongValue Encode(long l,String typeName)
	{
		return new JSONLongValue(l);
	}

	public static JSONLongValue Encode(Long l,String typeName)
	{
		return new JSONLongValue(l);
	}
	
	public static JSONStringValue Encode(String s,String typeName)
	{
		return new JSONStringValue(s);
	}
	
	public static JSONDoubleValue Encode(double d,String typeName)
	{
		return new JSONDoubleValue(d);
	}

	public static JSONDoubleValue Encode(Double d,String typeName)
	{
		return new JSONDoubleValue(d);
	}
	
	public static JSONValue Encode(Object o,String typeName)
	{
		if(o == null)
		{
			return IsAnArrayTypeName(typeName) ?emptyArray : emptyObject;
		}
		
		if(IsAnArrayType(o))
		{
			return EncodeArray(o,typeName);
		}
		else
		{
			if
			(
				o.getClass().equals(Integer.TYPE) || o.getClass().equals(Integer.class)
				|| o.getClass().equals(Long.TYPE) || o.getClass().equals(Long.class)
				|| o.getClass().equals(Short.TYPE) || o.getClass().equals(Short.class)
			)
			{
				return Encode(Long.parseLong(o.toString()),"long");
			}
			else if(o.getClass().equals(String.class))
			{
				return Encode(o.toString(),"String");
			}
			else if
			(
				o.getClass().equals(Float.TYPE) || o.getClass().equals(Float.class)
				|| o.getClass().equals(Double.TYPE) || o.getClass().equals(Double.class)
			)
			{
				return Encode(Double.parseDouble(o.toString()),"double");
			}
			else if
			(
				o.getClass().equals(Boolean.TYPE) || o.getClass().equals(Boolean.class)
			)
			{
				return Encode(Boolean.parseBoolean(o.toString()),"boolean");
			}
			
			JSONValue retVal = (JSONValue) InvokeEncoderMethod(o, typeName);
			return retVal != null ? retVal : emptyObject;
		}
	}

	public static boolean Decode(JSONBooleanValue jsonBooleanValue,String typeName)
	{
		return jsonBooleanValue != null ? jsonBooleanValue.GetValue() : false;
	}

	public static long Decode(JSONLongValue jsonLongValue,String typeName)
	{
		return jsonLongValue != null ? jsonLongValue.GetValue() : Long.MIN_VALUE;
	}

	public static double Decode(JSONDoubleValue jsonDoubleValue,String typeName)
	{
		return jsonDoubleValue != null ? jsonDoubleValue.GetValue() : Double.MIN_VALUE;
	}
	
	public static String Decode(JSONStringValue jsonStringValue,String typeName)
	{
		return jsonStringValue != null ? jsonStringValue.GetValue() : "";
	}
	
	@SuppressWarnings("unchecked")
	public static Object Decode(JSONArrayValue jsonArrayValue,String typeName)
	{
		final String componentTypeName = ParseComponentType(typeName);
		
		if(!typeName.endsWith("[]"))
		{
			final Collection list = typeName.indexOf("List") > 0 ? new ArrayList() : new HashSet();
			
			if(jsonArrayValue != null)
			{
				jsonArrayValue.ForEach
				(
					new JSONArrayValue.ForEach<Object>()
					{
						public void Do(int index,JSONValue value)
						{
							if
							(
								componentTypeName.endsWith("Long")
								&&
								value instanceof JSONLongValue
							)
							{
								list.add(JSONUtil.Decode((JSONLongValue)value,componentTypeName));
							}
							else if
							(
								componentTypeName.endsWith("Boolean")
								&&
								value instanceof JSONBooleanValue
							)
							{
								list.add((boolean)JSONUtil.Decode((JSONBooleanValue)value,componentTypeName));
							}
							else if
							(
								componentTypeName.endsWith("Integer")
								&&
								value instanceof JSONLongValue
							)
							{
								list.add((int)JSONUtil.Decode((JSONLongValue)value,componentTypeName));
							}
							else if
							(
								componentTypeName.endsWith("Short")
								&&
								value instanceof JSONLongValue
							)
							{
								list.add((short)JSONUtil.Decode((JSONLongValue)value,componentTypeName));
							}
							else if
							(
								componentTypeName.endsWith("Double")
								&&
								value instanceof JSONDoubleValue
							)
							{
								list.add(JSONUtil.Decode((JSONDoubleValue)value,componentTypeName));
							}
							else if
							(
								componentTypeName.endsWith("Float")
								&&
								value instanceof JSONDoubleValue
							)
							{
								list.add((float)JSONUtil.Decode((JSONDoubleValue)value,componentTypeName));
							}
							else if
							(
								componentTypeName.endsWith("String")
									&&
								value instanceof JSONStringValue
							)
							{
								list.add(JSONUtil.Decode((JSONStringValue)value,componentTypeName));
							}
							else if(value instanceof JSONArrayValue)
							{
								list.add(JSONUtil.Decode((JSONArrayValue)value,componentTypeName));
							}
							else
							{
								list.add(JSONUtil.Decode(value,componentTypeName));
							}
						}
					}
				);
			}			
			return list;
		}
		else
		{
			try
			{
				final Object[] array
					= (Object[]) Array.newInstance
						(
							TypeUtil.IsPrimitiveType(componentTypeName)
								? TypeUtil.GetPrimitiveType(componentTypeName)
								: Class.forName(componentTypeName),
							jsonArrayValue != null ? jsonArrayValue.GetNumEntries() : 0
						);
		
				if(jsonArrayValue != null)
				{
					jsonArrayValue.ForEach
					(
						new JSONArrayValue.ForEach<Object>()
						{
							public void Do(int index,JSONValue value)
							{
								if
								(
									(
										componentTypeName.equals("long")
									||
										componentTypeName.endsWith("Long")
									)
									&&
									(
										value instanceof JSONLongValue
									||
										value instanceof JSONStringValue
									)
								)
								{
									if(value instanceof JSONStringValue)
									{
										Array.set(array,index,Long.parseLong(((JSONStringValue)value).GetValue()));
									}
									else
									{
										Array.set(array,index,JSONUtil.Decode((JSONLongValue)value,componentTypeName));
									}
								}
								else if
								(
									(
										componentTypeName.equals("int")
									||
										componentTypeName.endsWith("Integer")
									)
									&&
									(
										value instanceof JSONLongValue
										||
										value instanceof JSONStringValue
									)
								)
								{
									if(value instanceof JSONStringValue)
									{
										Array.set(array,index,Integer.parseInt(((JSONStringValue)value).GetValue()));
									}
									else
									{
										Array.set(array,index,(int)JSONUtil.Decode((JSONLongValue)value,componentTypeName));
									}
								}
								else if
								(
									(
										componentTypeName.equals("boolean")
									||
										componentTypeName.endsWith("Boolean")
									)
									&&
									value instanceof JSONBooleanValue
								)
								{									
									Array.set(array,index,(boolean)JSONUtil.Decode((JSONBooleanValue)value,componentTypeName));
								}
								else if
								(
									(
										componentTypeName.equals("short")
									||
										componentTypeName.endsWith("Short")
									)
									&&
									(
											value instanceof JSONLongValue
											||
											value instanceof JSONStringValue
									)
								)
								{
									if(value instanceof JSONStringValue)
									{
										Array.set(array,index,Short.parseShort(((JSONStringValue)value).GetValue()));
									}
									else
									{
										Array.set(array,index,(short)JSONUtil.Decode((JSONLongValue)value,componentTypeName));
									}
								}
								else if
								(
									(
										componentTypeName.equals("double")
									||
										componentTypeName.endsWith("Double")
									)
									&&
									(
										value instanceof JSONDoubleValue
										||
										value instanceof JSONStringValue
									)
								)
								{
									if(value instanceof JSONStringValue)
									{
										Array.set(array,index,Double.parseDouble(((JSONStringValue)value).GetValue()));
									}
									else
									{
										Array.set(array,index,JSONUtil.Decode((JSONDoubleValue)value,componentTypeName));
									}
								}
								else if
								(
									(
										componentTypeName.equals("float")
									||
										componentTypeName.endsWith("Float")
									)
									&&
									(
										value instanceof JSONDoubleValue
										||
										value instanceof JSONStringValue
									)
								)
								{
									if(value instanceof JSONStringValue)
									{
										Array.set(array,index,Float.parseFloat(((JSONStringValue)value).GetValue()));
									}
									else
									{
										Array.set(array,index,(float)JSONUtil.Decode((JSONDoubleValue)value,componentTypeName));
									}
								}
								else if(componentTypeName.endsWith("String") && value instanceof JSONStringValue)
								{
									Array.set(array,index,JSONUtil.Decode((JSONStringValue)value,componentTypeName));
								}
								else if(value instanceof JSONArrayValue)
								{
									Array.set(array,index,JSONUtil.Decode((JSONArrayValue)value,componentTypeName));
								}
								else
								{
									Array.set(array,index,JSONUtil.Decode(value,componentTypeName));
								}
							}
						}
					);
				}
				
				return array;
			}
			catch (Throwable th)
			{
				throw new RuntimeException
				(
					"Failed to decode a " + typeName + ":" + th.getMessage(),
					th
				);
			}
		}
	}
	
	public static Object Decode(JSONValue jsonValue,String typeName)
	{
		if(jsonValue instanceof JSONArrayValue)
		{
			return Decode((JSONArrayValue)jsonValue,typeName);
		}
		
		return InvokeDecoderMethod(jsonValue, typeName);
	}

	public static synchronized void ScanClasspath()
	{
		PropertiesUtil.ScanClassPath
		(
			"json-mappers.properties",
			new PropertiesUtil.ForEach()
			{
				@Override
				public void Do(String key, String value)
				{
					RegisterJSONMapper(key);
				}
			}
		);
	}

	@SuppressWarnings("unchecked")
	public static void RegisterJSONMapper(String mapperTypeName)
	{
		Logger.logger.debug("Registering JSON Mapper methods from " + mapperTypeName + " ...");
		
		try
		{
			Class mapperType = Class.forName(mapperTypeName);

			for(Method method:mapperType.getMethods())
			{
				JSONEncoderMethod encoderAnnotation = method.getAnnotation(JSONEncoderMethod.class);
				JSONDecoderMethod decoderAnnotation = method.getAnnotation(JSONDecoderMethod.class);
				
				if(encoderAnnotation != null)
				{
					Class[] parameterTypes = method.getParameterTypes();
					if(parameterTypes.length != 1)
					{
						throw new RuntimeException
						(
							"Encoder methods can take only one parameter : the object to encode. " + 
							method.getDeclaringClass().getCanonicalName() + "." +  method.getName() + 
							" does not comply with this requirement."
						);
					}
					Logger.logger.debug("Encoder method " + method);
					encoderMethodByType.put(parameterTypes[0], method);
				}
				
				if(decoderAnnotation != null)
				{
					Class[] parameterTypes = method.getParameterTypes();
					if(!(parameterTypes.length >= 1 && parameterTypes.length <= 2))
					{
						throw new RuntimeException
						(
							"Decoder methods can take only one or two parameters : the JSON Object Value to decode(and the desiredtype as a Class object). " + 
							method.getDeclaringClass().getCanonicalName() + "." +  method.getName() + 
							" does not comply with this requirement."
						);
					}
					
					Logger.logger.debug("Decoder method " + method);
					decoderMethodByType.put(method.getReturnType(),method);
				}
			}
		}
		catch(Throwable th)
		{
			throw new RuntimeException
			(
				"Failed to register JSONMapper " + mapperTypeName + ":" + th.getMessage(),
				th
			);
		}
	}
	
	@SuppressWarnings("unchecked")
	private static Object InvokeEncoderMethod(Object o,String typeName)
	{
		if(o != null)
		{
			Method m = null;
			
			Class type1 = o.getClass();
			Class type2 = GetClassByName(typeName);
			
			while(m == null)
			{
				if(!type1.equals(Object.class))
				{
					m = encoderMethodByType.get(type1);
				}
				if(m == null && type2 != null && !type2.equals(Object.class))
				{
					m = encoderMethodByType.get(type2);
				}
				
				if(type1.equals(Object.class) && (type2 == null || type2.equals(Object.class)))
				{
					break;
				}
				else
				{
					if(!type1.equals(Object.class))
					{
						type1 = type1.getSuperclass();
					}
					if(type2 != null && !type2.equals(Object.class))
					{
						type2 = type2.getSuperclass();
					}
				}
			}
			
			if(m != null)
			{
				try
				{
					return m.invoke(null, new Object[]{o});
				}
				catch(Throwable th)
				{
					throw new RuntimeException
					(
						"Failed to invoke " + m.toString() + " on a " + o.getClass() + 
						",type:" + typeName + ":" + th.getMessage(),
						th
					);
				}
			}
		}
		
		return null;
	}
	
	@SuppressWarnings("unchecked")
	private static Object InvokeDecoderMethod(JSONValue jsonValue,String typeName)
	{
		if(jsonValue == null)
		{
			return null;
		}
		
		Class desiredType = GetClassByName(typeName);
		
		if(desiredType == null)
		{
			throw new RuntimeException
			(
				"Unknown type " + typeName + " passed to Decode."
			);
		}
		
		return InvokeDecoderMethod(jsonValue, desiredType,desiredType);
	}

	@SuppressWarnings("unchecked")
	private static Object InvokeDecoderMethod(JSONValue jsonValue,Class superType,Class desiredType)
	{
		Method m = decoderMethodByType.get(superType);
		if(m != null)
		{
			try
			{
				return
					(m.getParameterTypes().length == 1) 
						? 
							m.invoke(null, new Object[]{jsonValue})
						:
							m.invoke(null, new Object[]{jsonValue,desiredType})
					;				
			}
			catch(Throwable th)
			{
				throw new RuntimeException
				(
					"Failed to invoke " + m.toString() + " on a " + jsonValue.getClass() + 
					",type:" + superType + ":" + th.getMessage(),
					th
				);
			}
		}
		else
		{
			superType = superType.getSuperclass();
			if(superType.equals(Object.class))
			{
				return null;
			}
			else
			{
				return InvokeDecoderMethod(jsonValue, superType,desiredType);
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	private static Class GetClassByName(String typeName)
	{
		if(TypeUtil.IsPrimitiveType(typeName))
		{
			return TypeUtil.GetPrimitiveTypeRepresentative(typeName);
		}
		else
		{
			try
			{
				return Class.forName(typeName);
			}
			catch(Throwable th)
			{
				//ignore
				return null;
			}
		}
	}
	
	
	@SuppressWarnings("unchecked")
	private static JSONValue EncodeArray(Object o, String typeName)
	{
		String componetTypeName = ParseComponentType(typeName);
		
		ArrayList<JSONValue> entries = new ArrayList<JSONValue>();
		
		if(o instanceof List || o instanceof Set)
		{
			Collection list = (Collection)o;
			for (Iterator iterator = list.iterator(); iterator.hasNext();)
			{
				entries.add(Encode(iterator.next(),componetTypeName));
			}
		}
		else
		{
			Object[] array = (Object[])o;
			for (int i = 0; i < array.length; i++)
			{
				entries.add(Encode(array[i],componetTypeName));
			}
		}
		
		return new JSONArrayValue(entries);
	}

	private static String ParseComponentType(String typeName)
	{
		if
		(
			typeName.startsWith("java.util.")
			&&
			(
				typeName.indexOf("Set") > 0
				||
				typeName.indexOf("List") > 0
			)
		)
		{
			int firstLT = typeName.indexOf('<');
			int lastGT = typeName.lastIndexOf('>');
			
			if(firstLT > 0 && lastGT > 0)
			{
				return typeName.substring(firstLT + 1, lastGT);
			}
		}
		else if(typeName.endsWith("[]"))
		{
			return typeName.substring(0,typeName.length() - 2);
		}

		throw new RuntimeException("Dont know how to parse component type from '" + typeName + "'");
	}

	@SuppressWarnings("unchecked")
	private static boolean IsAnArrayType(Object o)
	{
		return (o != null) && ((o instanceof Set) || (o instanceof List)||(o.getClass().isArray())); 
	}

	private static boolean IsAnArrayTypeName(String typeName)
	{
		return  
		(
			typeName.startsWith("java.util.")
			&&
			(
				typeName.indexOf("Set") > 0
				||
				typeName.indexOf("List") > 0
			)
		)
		||(typeName.endsWith("[]"));
	}

	public static final JSONValue emptyObject = new JSONObjectValue();
	public static final JSONValue emptyArray = new JSONArrayValue();
	
	@SuppressWarnings("unchecked")
	private static HashMap<Class,Method> encoderMethodByType = new HashMap<Class, Method>();
	@SuppressWarnings("unchecked")
	private static HashMap<Class,Method> decoderMethodByType = new HashMap<Class, Method>();
}
